Skip to content

fix: Use singleton PostgresDBClient (Sqlalchemy engine) #321

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Jun 23, 2025

Conversation

yoomlam
Copy link
Contributor

@yoomlam yoomlam commented Jun 17, 2025

Ticket

https://navalabs.atlassian.net/browse/DST-1025
Test changes for https://navalabs.atlassian.net/browse/DST-1042

Changes

Create a single PostgresDBClient instance, and create sessions from that instance.

Reverts #317, so Promptfoo GH action should default back to 4 threads.

Fixed tests that indirectly create a DB session so that they use the app_config test fixture (which accesses the test DB schema) rather than the non-test app_config (which accesses the real DB schema).

Testing

Tested against Gemini LLM: #321 (comment)
and posted resulting DB connections: #321 (comment)

Preview environment for app

♻️ Environment destroyed ♻️

Copy link

Promptfoo Evaluation Results

Success Failure Total Pass Rate
11 4 15 73.33%

View detailed results in Google Sheets

» View eval results «

Comment on lines -121 to +123
promptfoo eval --max-concurrency 1 --config "/tmp/promptfooconfig.processed.yaml" --share --output "${OUTPUT_JSON_FILE}" --no-cache | tee "${EVAL_OUTPUT_FILE}"
promptfoo eval --config "/tmp/promptfooconfig.processed.yaml" --share --output "${OUTPUT_JSON_FILE}" --no-cache | tee "${EVAL_OUTPUT_FILE}"
else
promptfoo eval --max-concurrency 1 --config "/tmp/promptfooconfig.processed.yaml" --output "${OUTPUT_JSON_FILE}" --no-cache | tee "${EVAL_OUTPUT_FILE}"
promptfoo eval --config "/tmp/promptfooconfig.processed.yaml" --output "${OUTPUT_JSON_FILE}" --no-cache | tee "${EVAL_OUTPUT_FILE}"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverts #317, so it should default back to 4 threads

Comment on lines +45 to +47
@cached_property
def db_client(self) -> db.PostgresDBClient:
return db.PostgresDBClient()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This basically creates a singleton PostgresDBClient instance, which holds the Sqlalchemy engine.

“the engine is thread safe yes. individual Connection objects are not. we try to describe this at Working with Engines and Connections — SQLAlchemy 2.0 Documentation

Comment on lines -46 to +50
return db.PostgresDBClient().get_session()
return self.db_client.get_session()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new session uses an available connection (from the connection pool), so threads (e.g., those created from API calls) will not share a connection.

Our pool size is 20 – what happens when max sqlalchemy pool size is reached?

Google’s AI states:

Requests are queued: Any new requests for a database connection are placed in a queue, waiting for a connection to become available.

Timeout begins: SQLAlchemy starts a timeout period (defaulting to 30 seconds, but configurable) to see if a connection is released back into the pool.

Connection timeout error: If a connection doesn't become available within the timeout period, an exception (e.g., TimeoutError) is thrown, indicating a connection timeout.

SQLAlchemy connection pooling, what are checked out connections? : “If all the connections are simultaneously checked out then you can expect an error (there will be a timeout period during which SQLAlchemy waits to see if a connection gets freed up; this is also configurable).”

Copy link

Promptfoo Evaluation Results

Success Failure Total Pass Rate
11 4 15 73.33%

View detailed results in Google Sheets

» View eval results «

Copy link

Promptfoo Evaluation Results

Success Failure Total Pass Rate
11 4 15 73.33%

View detailed results in Google Sheets

» View eval results «

Copy link

Promptfoo Evaluation Results

Success Failure Total Pass Rate
11 4 15 73.33%

View detailed results in Google Sheets

» View eval results «

Copy link

github-actions bot commented Jun 17, 2025

☂️ Python Coverage

current status: ✅

Overall Coverage

Lines Covered Coverage Threshold Status
4282 3907 91% 80% 🟢

New Files

No new covered files...

Modified Files

File Coverage Status
app/src/app_config.py 83% 🟢
TOTAL 83% 🟢

updated for commit: a960bb4 by action🐍

Copy link

Promptfoo Evaluation Results

Success Failure Total Pass Rate
11 4 15 73.33%

View detailed results in Google Sheets

» View eval results «

Copy link

Promptfoo Evaluation Results

Success Failure Total Pass Rate
10 5 15 66.67%

View detailed results in Google Sheets

» View eval results «

@yoomlam
Copy link
Contributor Author

yoomlam commented Jun 18, 2025

@yoomlam
Copy link
Contributor Author

yoomlam commented Jun 18, 2025

DB connections spiked but went down:
image

Copy link

Promptfoo Evaluation Results

Success Failure Total Pass Rate
11 4 15 73.33%

View detailed results in Google Sheets

» View eval results «

@yoomlam yoomlam changed the base branch from main to yl/add-gemini-llm June 18, 2025 17:53
@yoomlam yoomlam marked this pull request as ready for review June 23, 2025 14:22
@yoomlam yoomlam requested a review from Copilot June 23, 2025 14:26
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR centralizes database session management by introducing a singleton PostgresDBClient, updates tests to use the app_config fixture for obtaining test sessions, and reverts the Promptfoo GH action to its default concurrency.

  • Introduce a cached db_client in AppConfig and route db_session through it
  • Update pytest fixtures and test signatures to include app_config
  • Remove explicit --max-concurrency flags in Promptfoo workflow to restore default threading

Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

File Description
app/src/app_config.py Added db_client cached property, updated db_session to use it
app/tests/conftest.py Changed app_config fixture to depend on db_client
app/tests/src/test_retrieve.py and other tests Updated test signatures to include app_config and use chunk.id
.github/workflows/promptfoo-googlesheet-evaluation.yml Removed --max-concurrency flags to revert to default concurrency
Comments suppressed due to low confidence (2)

app/src/app_config.py:45

  • The cached_property decorator is used but not imported; add from functools import cached_property at the top of the file.
    @cached_property

app/tests/conftest.py:111

  • The db_client fixture is referenced here but not defined; consider adding a db_client pytest fixture or switch back to using the existing db_session fixture.
def app_config(monkeypatch, db_client: db.DBClient):

Base automatically changed from yl/add-gemini-llm to main June 23, 2025 14:37
Copy link

Promptfoo Evaluation Results

Success Failure Total Pass Rate
11 4 15 73.33%

View detailed results in Google Sheets

» View eval results «

Copy link

Promptfoo Evaluation Results

Success Failure Total Pass Rate
11 4 15 73.33%

View detailed results in Google Sheets

» View eval results «

assert results[0].chunk == short_chunk
assert results[0].chunk.id == short_chunk.id
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for these changes? Because the db_session from the test fixture (the second parameter to test_retrieve_with_scores) is different from the db_session generated by retrieve_with_scores?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. There's some identifier in the chunk instances that is specific to the db_session, so those identifiers are different and cause the assertion to fail. Otherwise the chunks are identical.

Copy link
Contributor

@KevinJBoyer KevinJBoyer Jun 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. I think the identifier here is the literal address in memory: under the hood, SQLAlchemy ensures that if you retrieve the same row back in the same session, it creates only a single instance of the object in memory so you can do things like (psuedocode):

with some_session:
    # in this call SQLAlchemy creates a new instance of the Chunk class and returns that
    chunk_1 = some_session.select(chunk_id="abc").first()
    
    # in this call SQLAlchemy will recognize that it already has an instance of the Chunk class for this row, so it will return that Chunk
    chunk_2 = some_session.select(chunk_id="abc").first()
    assert chunk_1 == chunk_2 # these are literally the same object in memory

Copy link
Contributor

@KevinJBoyer KevinJBoyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just had a question to make sure I understood changes to the test correctly. TY!

@yoomlam yoomlam merged commit fd366a0 into main Jun 23, 2025
13 checks passed
@yoomlam yoomlam deleted the yl/pgclient-singleton branch June 23, 2025 16:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants